1   package org.apache.lucene.geo3d;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one or more
5    * contributor license agreements.  See the NOTICE file distributed with
6    * this work for additional information regarding copyright ownership.
7    * The ASF licenses this file to You under the Apache License, Version 2.0
8    * (the "License"); you may not use this file except in compliance with
9    * the License.  You may obtain a copy of the License at
10   *
11   *     http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   */
19  
20  import java.util.ArrayList;
21  import java.util.BitSet;
22  import java.util.List;
23  
24  /**
25   * GeoConvexPolygon objects are generic building blocks of more complex structures.
26   * The only restrictions on these objects are: (1) they must be convex; (2) they must have
27   * a maximum extent no larger than PI.  Violating either one of these limits will
28   * cause the logic to fail.
29   *
30   * @lucene.experimental
31   */
32  public class GeoConvexPolygon extends GeoBasePolygon {
33    /** The list of polygon points */
34    protected final List<GeoPoint> points;
35    /** A bitset describing, for each edge, whether it is internal or not */
36    protected final BitSet isInternalEdges;
37  
38    /** A list of edges */
39    protected SidedPlane[] edges = null;
40    /** The set of notable points for each edge */
41    protected GeoPoint[][] notableEdgePoints = null;
42    /** A point which is on the boundary of the polygon */
43    protected GeoPoint[] edgePoints = null;
44    /** Tracking the maximum distance we go at any one time, so to be sure it's legal */
45    protected double fullDistance = 0.0;
46    /** Set to true when the polygon is complete */
47    protected boolean isDone = false;
48    
49    /**
50     * Create a convex polygon from a list of points.  The first point must be on the
51     * external edge.
52     *@param planetModel is the planet model.
53     *@param pointList is the list of points to create the polygon from.
54     */
55    public GeoConvexPolygon(final PlanetModel planetModel, final List<GeoPoint> pointList) {
56      super(planetModel);
57      this.points = pointList;
58      this.isInternalEdges = new BitSet();
59      done(false);
60    }
61  
62    /**
63     * Create a convex polygon from a list of points, keeping track of which boundaries
64     * are internal.  This is used when creating a polygon as a building block for another shape.
65     *@param planetModel is the planet model.
66     *@param pointList is the set of points to create the polygon from.
67     *@param internalEdgeFlags is a bitset describing whether each edge is internal or not.
68     *@param returnEdgeInternal is true when the final return edge is an internal one.
69     */
70    public GeoConvexPolygon(final PlanetModel planetModel, final List<GeoPoint> pointList, final BitSet internalEdgeFlags,
71                            final boolean returnEdgeInternal) {
72      super(planetModel);
73      this.points = pointList;
74      this.isInternalEdges = internalEdgeFlags;
75      done(returnEdgeInternal);
76    }
77  
78    /**
79     * Create a convex polygon, with a starting latitude and longitude.
80     * Accepts only values in the following ranges: lat: {@code -PI/2 -> PI/2}, lon: {@code -PI -> PI}
81     *@param planetModel is the planet model.
82     *@param startLatitude is the latitude of the first point.
83     *@param startLongitude is the longitude of the first point.
84     */
85    public GeoConvexPolygon(final PlanetModel planetModel, final double startLatitude, final double startLongitude) {
86      super(planetModel);
87      points = new ArrayList<>();
88      isInternalEdges = new BitSet();
89      points.add(new GeoPoint(planetModel, startLatitude, startLongitude));
90    }
91  
92    /**
93     * Add a point to the polygon.
94     * Accepts only values in the following ranges: lat: {@code -PI/2 -> PI/2}, lon: {@code -PI -> PI}
95     *
96     * @param latitude       is the latitude of the next point.
97     * @param longitude      is the longitude of the next point.
98     * @param isInternalEdge is true if the edge just added with this point should be considered "internal", and not
99     *                       intersected as part of the intersects() operation.
100    */
101   public void addPoint(final double latitude, final double longitude, final boolean isInternalEdge) {
102     if (isDone)
103       throw new IllegalStateException("Can't call addPoint() if done() already called");
104     if (isInternalEdge)
105       isInternalEdges.set(points.size() - 1);
106     points.add(new GeoPoint(planetModel, latitude, longitude));
107   }
108 
109   /**
110    * Finish the polygon, by connecting the last added point with the starting point.
111    *@param isInternalReturnEdge is true if the return edge (back to start) is an internal one.
112    */
113   public void done(final boolean isInternalReturnEdge) {
114     if (isDone)
115       throw new IllegalStateException("Can't call done() more than once");
116     // If fewer than 3 points, can't do it.
117     if (points.size() < 3)
118       throw new IllegalArgumentException("Polygon needs at least three points.");
119 
120     if (isInternalReturnEdge)
121       isInternalEdges.set(points.size() - 1);
122 
123     isDone = true;
124     
125     // Time to construct the planes.  If the polygon is truly convex, then any adjacent point
126     // to a segment can provide an interior measurement.
127     edges = new SidedPlane[points.size()];
128     notableEdgePoints = new GeoPoint[points.size()][];
129 
130     for (int i = 0; i < points.size(); i++) {
131       final GeoPoint start = points.get(i);
132       final GeoPoint end = points.get(legalIndex(i + 1));
133       final double distance = start.arcDistance(end);
134       if (distance > fullDistance)
135         fullDistance = distance;
136       final GeoPoint check = points.get(legalIndex(i + 2));
137       final SidedPlane sp = new SidedPlane(check, start, end);
138       //System.out.println("Created edge "+sp+" using start="+start+" end="+end+" check="+check);
139       edges[i] = sp;
140       notableEdgePoints[i] = new GeoPoint[]{start, end};
141     }
142     createCenterPoint();
143   }
144 
145   /** Compute a reasonable center point.
146    */
147   protected void createCenterPoint() {
148     // In order to naively confirm that the polygon is convex, I would need to
149     // check every edge, and verify that every point (other than the edge endpoints)
150     // is within the edge's sided plane.  This is an order n^2 operation.  That's still
151     // not wrong, though, because everything else about polygons has a similar cost.
152     for (int edgeIndex = 0; edgeIndex < edges.length; edgeIndex++) {
153       final SidedPlane edge = edges[edgeIndex];
154       for (int pointIndex = 0; pointIndex < points.size(); pointIndex++) {
155         if (pointIndex != edgeIndex && pointIndex != legalIndex(edgeIndex + 1)) {
156           if (!edge.isWithin(points.get(pointIndex)))
157             throw new IllegalArgumentException("Polygon is not convex: Point " + points.get(pointIndex) + " Edge " + edge);
158         }
159       }
160     }
161     edgePoints = new GeoPoint[]{points.get(0)};
162   }
163 
164   /** Compute a legal point index from a possibly illegal one, that may have wrapped.
165    *@param index is the index.
166    *@return the normalized index.
167    */
168   protected int legalIndex(int index) {
169     while (index >= points.size())
170       index -= points.size();
171     return index;
172   }
173 
174   @Override
175   public boolean isWithin(final double x, final double y, final double z) {
176     for (final SidedPlane edge : edges) {
177       if (!edge.isWithin(x, y, z))
178         return false;
179     }
180     return true;
181   }
182 
183   @Override
184   public GeoPoint[] getEdgePoints() {
185     return edgePoints;
186   }
187 
188   @Override
189   public boolean intersects(final Plane p, final GeoPoint[] notablePoints, final Membership... bounds) {
190     //System.err.println("Checking for polygon intersection with plane "+p+"...");
191     for (int edgeIndex = 0; edgeIndex < edges.length; edgeIndex++) {
192       final SidedPlane edge = edges[edgeIndex];
193       final GeoPoint[] points = this.notableEdgePoints[edgeIndex];
194       if (!isInternalEdges.get(edgeIndex)) {
195         //System.err.println(" non-internal edge "+edge);
196         // Edges flagged as 'internal only' are excluded from the matching
197         // Construct boundaries
198         final Membership[] membershipBounds = new Membership[edges.length - 1];
199         int count = 0;
200         for (int otherIndex = 0; otherIndex < edges.length; otherIndex++) {
201           if (otherIndex != edgeIndex) {
202             membershipBounds[count++] = edges[otherIndex];
203           }
204         }
205         if (edge.intersects(planetModel, p, notablePoints, points, bounds, membershipBounds)) {
206           //System.err.println(" intersects!");
207           return true;
208         }
209       }
210     }
211     //System.err.println(" no intersection");
212     return false;
213   }
214 
215   @Override
216   public void getBounds(Bounds bounds) {
217     super.getBounds(bounds);
218 
219     // Add all the points
220     for (final GeoPoint point : points) {
221       bounds.addPoint(point);
222     }
223 
224     // Add planes with membership.
225     for (int edgeIndex = 0; edgeIndex < edges.length; edgeIndex++) {
226       final SidedPlane edge = edges[edgeIndex];
227       // Construct boundaries
228       final Membership[] membershipBounds = new Membership[edges.length - 1];
229       int count = 0;
230       for (int otherIndex = 0; otherIndex < edges.length; otherIndex++) {
231         if (otherIndex != edgeIndex) {
232           membershipBounds[count++] = edges[otherIndex];
233         }
234       }
235       bounds.addPlane(planetModel, edge, membershipBounds);
236     }
237   }
238 
239   @Override
240   protected double outsideDistance(final DistanceStyle distanceStyle, final double x, final double y, final double z) {
241     double minimumDistance = Double.MAX_VALUE;
242     for (final GeoPoint edgePoint : points) {
243       final double newDist = distanceStyle.computeDistance(edgePoint, x,y,z);
244       if (newDist < minimumDistance) {
245         minimumDistance = newDist;
246       }
247     }
248     for (int edgeIndex = 0; edgeIndex < edges.length; edgeIndex++) {
249       final Plane edgePlane = edges[edgeIndex];
250       final Membership[] membershipBounds = new Membership[edges.length - 1];
251       int count = 0;
252       for (int otherIndex = 0; otherIndex < edges.length; otherIndex++) {
253         if (otherIndex != edgeIndex) {
254           membershipBounds[count++] = edges[otherIndex];
255         }
256       }
257       final double newDist = distanceStyle.computeDistance(planetModel, edgePlane, x, y, z, membershipBounds);
258       if (newDist < minimumDistance) {
259         minimumDistance = newDist;
260       }
261     }
262     return minimumDistance;
263   }
264 
265   @Override
266   public boolean equals(Object o) {
267     if (!(o instanceof GeoConvexPolygon))
268       return false;
269     GeoConvexPolygon other = (GeoConvexPolygon) o;
270     if (!super.equals(other))
271       return false;
272     if (!other.isInternalEdges.equals(isInternalEdges))
273       return false;
274     return (other.points.equals(points));
275   }
276 
277   @Override
278   public int hashCode() {
279     int result = super.hashCode();
280     result = 31 * result + points.hashCode();
281     return result;
282   }
283 
284   @Override
285   public String toString() {
286     return "GeoConvexPolygon: {planetmodel=" + planetModel + ", points=" + points + "}";
287   }
288 }
289